

import { crc32 } from "./crc32";
import * as deflate from "./deflate-js";

// magic numbers marking this file as GZIP
var ID1 = 0x1F,
ID2 = 0x8B,
compressionMethods = {
	'deflate': 8
},
possibleFlags = {
	'FTEXT': 0x01,
	'FHCRC': 0x02,
	'FEXTRA': 0x04,
	'FNAME': 0x08,
	'FCOMMENT': 0x10
},
osMap = {
	'fat': 0, // FAT file system (DOS, OS/2, NT) + PKZIPW 2.50 VFAT, NTFS
	'amiga': 1, // Amiga
	'vmz': 2, // VMS (VAX or Alpha AXP)
	'unix': 3, // Unix
	'vm/cms': 4, // VM/CMS
	'atari': 5, // Atari
	'hpfs': 6, // HPFS file system (OS/2, NT 3.x)
	'macintosh': 7, // Macintosh
	'z-system': 8, // Z-System
	'cplm': 9, // CP/M
	'tops-20': 10, // TOPS-20
	'ntfs': 11, // NTFS file system (NT)
	'qdos': 12, // SMS/QDOS
	'acorn': 13, // Acorn RISC OS
	'vfat': 14, // VFAT file system (Win95, NT)
	'vms': 15, // MVS (code also taken for PRIMOS)
	'beos': 16, // BeOS (BeBox or PowerMac)
	'tandem': 17, // Tandem/NSK
	'theos': 18 // THEOS
},
os = 'unix',
DEFAULT_LEVEL = 6;

function putByte(n, arr) {
	arr.push(n & 0xFF);
}

// LSB first
function putShort(n, arr) {
	arr.push(n & 0xFF);
	arr.push(n >>> 8);
}

// LSB first
function putLong(n, arr) {
	putShort(n & 0xffff, arr);
	putShort(n >>> 16, arr);
}

function putString(s, arr) {
	var i, len = s.length;
	for (i = 0; i < len; i += 1) {
		putByte(s.charCodeAt(i), arr);
	}
}

function readByte(arr) {
	return arr.shift();
}

function readShort(arr) {
	return arr.shift() | (arr.shift() << 8);
}

function readLong(arr) {
	var n1 = readShort(arr),
		n2 = readShort(arr);

	// JavaScript can't handle bits in the position 32
	// we'll emulate this by removing the left-most bit (if it exists)
	// and add it back in via multiplication, which does work
	if (n2 > 32768) {
		n2 -= 32768;

		return ((n2 << 16) | n1) + 32768 * Math.pow(2, 16);
	}

	return (n2 << 16) | n1;
}

function readString(arr) {
	var charArr = [];

	// turn all bytes into chars until the terminating null
	while (arr[0] !== 0) {
		charArr.push(String.fromCharCode(arr.shift()));
	}

	// throw away terminating null
	arr.shift();

	// join all characters into a cohesive string
	return charArr.join('');
}

/*
* Reads n number of bytes and return as an array.
*
* @param arr- Array of bytes to read from
* @param n- Number of bytes to read
*/
function readBytes(arr, n) {
	var i, ret = [];
	for (i = 0; i < n; i += 1) {
		ret.push(arr.shift());
	}

	return ret;
}

/*
* ZIPs a file in GZIP format. The format is as given by the spec, found at:
* http://www.gzip.org/zlib/rfc-gzip.html
*
* Omitted parts in this implementation:
*/
export function zip(data, options) {
	var flags = 0,
		level,
		crc, out = [];

	if (!options) {
		options = {};
	}
	level = options.level || DEFAULT_LEVEL;

	if (typeof data === 'string') {
		data = Array.prototype.map.call(data, function (char) {
			return char.charCodeAt(0);
		});
	}

	// magic number marking this file as GZIP
	putByte(ID1, out);
	putByte(ID2, out);

	putByte(compressionMethods['deflate'], out);

	if (options.name) {
		flags |= possibleFlags['FNAME'];
	}

	putByte(flags, out);
	putLong(options.timestamp || parseInt(Date.now() / 1000, 10), out);

	// put deflate args (extra flags)
	if (level === 1) {
		// fastest algorithm
		putByte(4, out);
	} else if (level === 9) {
		// maximum compression (fastest algorithm)
		putByte(2, out);
	} else {
		putByte(0, out);
	}

	// OS identifier
	putByte(osMap[os], out);

	if (options.name) {
		// ignore the directory part
		putString(options.name.substring(options.name.lastIndexOf('/') + 1), out);

		// terminating null
		putByte(0, out);
	}

	deflate.deflate(data, level).forEach(function (byte) {
		putByte(byte, out);
	});

	putLong(parseInt(crc32(data), 16), out);
	putLong(data.length, out);

	return out;
}

export function unzip(data) {
// start with a copy of the array
	var arr = Array.prototype.slice.call(data, 0),
	t,
	compressionMethod,
	flags,
	mtime,
	xFlags,
	key,
	os,
	crc,
	size,
	res;

	// check the first two bytes for the magic numbers
	if (readByte(arr) !== ID1 || readByte(arr) !== ID2) {
		throw 'Not a GZIP file';
	}

	t = readByte(arr);
	t = Object.keys(compressionMethods).some(function (key) {
		compressionMethod = key;
		return compressionMethods[key] === t;
	});

	if (!t) {
		throw 'Unsupported compression method';
	}

	flags = readByte(arr);
	mtime = readLong(arr);
	xFlags = readByte(arr);
	t = readByte(arr);
	Object.keys(osMap).some(function (key) {
		if (osMap[key] === t) {
			os = key;
			return true;
		}
	});

	// just throw away the bytes for now
	if (flags & possibleFlags['FEXTRA']) {
		t = readShort(arr);
		readBytes(arr, t);
	}

	// just throw away for now
	if (flags & possibleFlags['FNAME']) {
		readString(arr);
	}

	// just throw away for now
	if (flags & possibleFlags['FCOMMENT']) {
		readString(arr);
	}

	// just throw away for now
	if (flags & possibleFlags['FHCRC']) {
		readShort(arr);
	}

	if (compressionMethod === 'deflate') {
		// give deflate everything but the last 8 bytes
		// the last 8 bytes are for the CRC32 checksum and filesize
		res = deflate.inflate(arr.splice(0, arr.length - 8));
	}

	if (flags & possibleFlags['FTEXT']) {
		res = Array.prototype.map.call(res, function (byte) {
			return String.fromCharCode(byte);
		}).join('');
	}

	crc = readLong(arr) >>> 0;
	if (crc !== parseInt(crc32(res), 16)) {
		throw 'Checksum does not match';
	}

	size = readLong(arr);
	if (size !== res.length) {
		throw 'Size of decompressed file not correct';
	}

	return res;
}


